设计模式(二)— 适配器(Adapter)模式

一.基本介绍

著名的设计模式“四人帮”这样评价适配器模式:

将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。—— Gang of Four

适配器模式将一个类的接口适配成用户所期待的。一个适配器通常允许因为接口不兼容而不能一起工作的类能够在一起工作,做法是将类自己的接口包裹在一个已存在的类中。

Adapter 设计模式主要目的组合两个不相干类,常用有两种方法,第一种解决方案是修改各自类的接口。但是如果没有源码,或者不愿意为了一个应用而修改各自的接口,则需要使用 Adapter 适配器,在两种接口之间创建一个混合接口。

图1.适配器模式类图

图1所示是适配器模式的类图。Adapter 适配器设计模式中有 3 个重要角色:被适配者 Adaptee,适配器 Adapter 和目标对象 Target。其中两个现存的想要组合到一起的类分别是被适配者 Adaptee 和目标对象 Target 角色,按照类图所示,我们需要创建一个适配器 Adapter 将其组合在一起。

具体实现代码如下。

客户端使用的接口:Target

1
2
3
4
5
6
7
8
9
10
11
/**
* 客户端请求的目标接口,与实际业务相关
*/
public interface Target {

/**
* 此方法处理客户端的请求,由目标接口的实现类实现
*/
public void dealRequest();

}

客户端使用的接口实现类:TargetImpl

1
2
3
4
5
6
7
8
public class TargetImpl implements Target {

@Override
public void dealRequest() {
System.out.println("处理来自客户端的请求-------");
}

}

被适配的对象:Adaptee

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 已经存在的接口,此(实现)类中原本存在一个方法可以处理客户端的特殊请求
*/
public class Adaptee {


public void dealSpecialRequest() {
//业务代码
System.out.println("处理客户端的特殊请求++++++++");
}

}

适配器实现:Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* 适配器类
*/
public class Adapter implements Target{

/*
* 持有需要被适配的接口对象
*/
private Adaptee adaptee;

/*
* 构造方法,传入需要被适配的对象
* @param adaptee 需要被适配的对象
*/
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}

@Override
public void request() {
// TODO Auto-generated method stub
adaptee.specificRequest();
}

}

客户端请求代码模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* 使用适配器的客户端
*/
public class Client {

public static void main(String[] args) {
Target target = previousDealWay();
// Target target = currentDealWay();
target.dealRequest();
}

public static Target previousDealWay() {
Target target = new TargetImpl();
return target;
}

public static Target currentDealWay() {
Adaptee adaptee = new Adaptee();
Target target = new Adapter(adaptee);
return target;
}
}

以下情况比较适合使用 Adapter 模式:

  • 当你想使用一个已经存在的类,而它的接口不符合你的需求;
  • 你想创建一个可以复用的类,该类可以与其他不相关的类或不可预见的类协同工作;
  • 你想使用一些已经存在的子类,但是不可能对每一个都进行子类化以匹配它们的接口,对象适配器可以适配它的父接口。

二.实际应用举例

考虑一个记录日志的应用,用户可能会提出要求采用文件的方式存储日志,也可能会提出存储日志到数据库的需求,这样我们可以采用适配器模式对旧的日志类进行改造,提供新的支持方式。

首先我们需要一个简单的日志对象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* 日志数据对象
*/
ppublic class LogBean {

/* 日志编号 */
private String logId;

/* 操作人id */
private String optUserId;

public LogBean() {
}

public LogBean(String logId, String optUserId) {
this.logId = logId;
this.optUserId = optUserId;
}

public String getLogId() {
return logId;
}

public void setLogId(String logId) {
this.logId = logId;
}

public String getOptUserId() {
return optUserId;
}

public void setOptUserId(String optUserId) {
this.optUserId = optUserId;
}

@Override
public String toString() {
return "LogBean{" +
"logId='" + logId + '\'' +
", optUserId='" + optUserId + '\'' +
'}';
}
}

接下来定义一个操作日志文件的接口

1
2
3
4
5
6
7
8
public interface LogFileOperateApi {

/**
* 写日志文件,把日志列表写出到日志文件中去
* @param list
*/
public void writeLogFile(List<LogBean> list);
}

在写一个操作日志文件的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class LogFileOperate implements LogFileOperateApi {

/*
* 设置日志文件的路径和文件名称
*/
private String logFileName = "/temp/file.log";

/*
* 构造方法,传入文件的路径和名称
*/
public LogFileOperate(String logFilename) {
if (logFilename != null) {
this.logFileName = logFilename;
}
}


@Override
public void writeLogFile(List<LogBean> list) {
for (LogBean logBean : list) {
System.out.println("将日志记录到日志文件" + logFileName + "|" + logBean.toString());
}
}
}

如果这时候需要引入数据库方式,引入适配器之前,我们需要定义日志管理的操作接口(此处为方便,直接写实现类):

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LogDbOperate {

/**
* 新增日志
*
* @param logbean
*/
public void createLog(LogBean logbean) {
System.out.println("日志记录到数据库|" + logbean.toString());
}


}

接下来就要实现适配器了,LogFileOperate接口就相当于 Target 接口,LogDbOperate 就相当于 Adaptee 类。Adapter 类代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 日志适配器类
*
* @author liaosi
* @date 2019-02-23
*/
public class LogAdapter implements LogFileOperateApi {

private LogDbOperate logDbOperate;


public LogAdapter(LogDbOperate adptee) {
logDbOperate = adptee;
}

@Override
public void writeLogFile(List<LogBean> list) {
for (LogBean logBean : list) {
logDbOperate.createLog(logBean);
}
}

}

最后是客户端代码的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 调用日志操作的客户端类
*
* @author liaosi
* @date 2019-02-23
*/
public class Client {

private LogFileOperateApi logFileOperateApi;

@Before
public void init() {
logFileOperateApi = new LogAdapter(new LogDbOperate());
}


@Test
public void clientLogOperate() {
List<LogBean> logBeanList = new ArrayList<LogBean>();
logBeanList.add(new LogBean("1", "michael"));
logFileOperateApi.writeLogFile(logBeanList);
}

}

三.适配器模式在JDK中的应用

集合内元素的访问

JDK1.1 之前提供的容器有 Arrays,Vector,Stack,Hashtable,Properties,BitSet,其中定义了一种访问群集内各元素的标准方式,称为 Enumeration(列举器)接口,用法如下所示。

1
2
3
4
5
Vector v=new Vector();
for (Enumeration enum =v.elements(); enum.hasMoreElements();) {
Object o = enum.nextElement();
processObject(o);
}

JDK1.2 版本中引入了 Iterator 接口,新版本的集合对象(HashSet,HashMap,WeakHeahMap,ArrayList,TreeSet,TreeMap, LinkedList)是通过 Iterator 接口访问集合元素的,用法如下。

1
2
3
4
List list=new ArrayList();
for(Iterator it=list.iterator();it.hasNext();){
System.out.println(it.next());
}

这样,如果将老版本的程序运行在新的 Java 编译器上就会出错。因为 List 接口中已经没有 elements(),而只有 iterator() 了。那么如何将老版本的程序运行在新的 Java 编译器上呢? 如果不加修改,是肯定不行的,但是修改要遵循“开-闭”原则。我们可以用 Java 设计模式中的适配器模式解决这个问题。
采用如下的适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

public class NewEnumeration implements Enumeration {

Iterator it;

public NewEnumeration(Iterator it) {
this.it = it;
// TODO Auto-generated constructor stub
}

public boolean hasMoreElements() {
// TODO Auto-generated method stub
return it.hasNext();
}

public Object nextElement() {
// TODO Auto-generated method stub
return it.next();
}

public static void main(String[] args) {
List list = new ArrayList();
list.add("a");
list.add("b");
list.add("C");
for (Enumeration e = new NewEnumeration(list.iterator()); e.hasMoreElements(); ) {
System.out.println(e.nextElement());
}
}
}

NewEnumeration 是一个适配器类,通过它实现了从 Iterator 接口到 Enumeration 接口的适配,这样我们就可以使用老版本的代码来使用新的集合对象了。

I/O库

Java I/O 库大量使用了适配器模式,例如 ByteArrayInputStream 是一个适配器类,它继承了 InputStream 的接口,并且封装了一个 byte 数组。换言之,它将一个 byte 数组的接口适配成 InputStream 流处理器的接口。

我们知道 Java 语言支持四种类型:Java 接口,Java 类,Java 数组,原始类型(即 int,float 等)。前三种是引用类型,类和数组的实例是对象,原始类型的值不是对象。也即,Java 语言的数组是像所有的其他对象一样的对象,而不管数组中所存储的元素类型是什么。这样一来的话,ByteArrayInputStream 就符合适配器模式的描述,是一个对象形式的适配器类。

FileInputStream 是一个适配器类。在 FileInputStream 继承了 InputStrem 类型,同时持有一个对 FileDiscriptor 的引用。这是将一个 FileDiscriptor 对象适配成 InputStrem 类型的对象形式的适配器模式。查看 JDK1.4 的源代码我们可以看到清单 15 所示的 FileInputStream 类的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class FileInputStream extends InputStream {

/* File Descriptor - handle to the open file */
private final FileDescriptor fd;

public FileInputStream(File file) throws FileNotFoundException {
String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkRead(name);
}
if (name == null) {
throw new NullPointerException();
}
if (file.isInvalid()) {
throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}

public FileInputStream(FileDescriptor fdObj) {
SecurityManager security = System.getSecurityManager();
if (fdObj == null) {
throw new NullPointerException();
}
if (security != null) {
security.checkRead(fdObj);
}
fd = fdObj;
path = null;

/*
* FileDescriptor is being shared by streams.
* Register this stream with FileDescriptor tracker.
*/
fd.attach(this);
}

//其它代码
}

同样地,在 OutputStream 类型中,所有的原始流处理器都是适配器类。ByteArrayOutputStream 继承了 OutputStream 类型,同时持有一个对 byte 数组的引用。它一个 byte 数组的接口适配成 OutputString 类型的接口,因此也是一个对象形式的适配器模式的应用。
FileOutputStream 继承了 OutputStream 类型,同时持有一个对 FileDiscriptor 对象的引用。这是一个将 FileDiscriptor 接口适配成 OutputStream 接口形式的对象型适配器模式。
Reader 类型的原始流处理器都是适配器模式的应用。StringReader 是一个适配器类,StringReader 类继承了 Reader 类型,持有一个对 String 对象的引用。它将 String 的接口适配成 Reader 类型的接口。

------ 本文完 ------